Bootstrap demo

Python User-Defined Functions

A detailed reference for creating and using functions in Python

Table of Contents

Introduction to Functions

Definition: A function in Python is a reusable block of code that performs a specific task. Functions help break our program into smaller, modular chunks, making it more organized and manageable. They allow code reuse and avoid repetition.

Benefits of using functions

  • Code reusability
  • Improved readability
  • Easier debugging and testing
  • Modular programming
  • Abstraction of complex operations

Defining Functions

Basic Function Syntax

def function_name(parameters):
    """docstring - optional documentation"""
    # function body
    # code statements
    return value  # optional

Simple Function Example

def greet():
    """This function greets the user"""
    print("Hello, welcome to Python programming!")

# Calling the function
greet()  # Output: Hello, welcome to Python programming!

Function with Parameters

def greet_user(name):
    """Greet a specific user"""
    print(f"Hello {name}, welcome to Python programming!")

greet_user("Alice")  # Output: Hello Alice, welcome to Python programming!
greet_user("Bob")    # Output: Hello Bob, welcome to Python programming!

Function Parameters

Positional Parameters

def describe_pet(animal_type, pet_name):
    """Display information about a pet"""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('hamster', 'harry')
# Output:
# I have a hamster.
# My hamster's name is Harry.

Keyword Arguments

def describe_pet(animal_type, pet_name):
    """Display information about a pet"""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

# Using keyword arguments (order doesn't matter)
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='dog', pet_name='willie')

Default Parameter Values

def describe_pet(pet_name, animal_type='dog'):
    """Display information about a pet with default type"""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet(pet_name='willie')  # Uses default animal_type
describe_pet('harry', 'hamster')  # Overrides default

Arbitrary Arguments: *args and **kwargs

# *args for arbitrary positional arguments
def make_pizza(*toppings):
    """Summarize the pizza we are about to make"""
    print("\nMaking a pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

# **kwargs for arbitrary keyword arguments
def build_profile(first, last, **user_info):
    """Build a dictionary containing everything we know about a user"""
    user_info['first_name'] = first
    user_info['last_name'] = last
    return user_info

user_profile = build_profile('albert', 'einstein',
                             location='princeton',
                             field='physics')
print(user_profile)
# Output: {'location': 'princeton', 'field': 'physics', 
#          'first_name': 'albert', 'last_name': 'einstein'}

Return Values

Returning Simple Values

def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted"""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')
print(musician)  # Output: Jimi Hendrix

Returning Multiple Values

def get_user_info():
    """Return multiple values as a tuple"""
    name = "Alice"
    age = 30
    occupation = "Engineer"
    return name, age, occupation  # Returns a tuple

user_data = get_user_info()
print(user_data)  # Output: ('Alice', 30, 'Engineer')

# Unpacking returned values
name, age, occupation = get_user_info()
print(f"{name} is {age} years old and works as an {occupation}")

Returning Complex Data Structures

def build_car(manufacturer, model, **car_info):
    """Build a dictionary storing information about a car"""
    car_info['manufacturer'] = manufacturer
    car_info['model'] = model
    return car_info

car = build_car('subaru', 'outback', color='blue', tow_package=True)
print(car)
# Output: {'color': 'blue', 'tow_package': True, 
#          'manufacturer': 'subaru', 'model': 'outback'}

Variable Scope

Local Variables

def test_local_scope():
    local_var = "I'm local to this function"
    print(local_var)

test_local_scope()  # Output: I'm local to this function
# print(local_var)  # This would cause an error - NameError

Global Variables

global_var = "I'm a global variable"

def test_global_scope():
    print(global_var)  # Can access global variables

test_global_scope()  # Output: I'm a global variable

The global Keyword

counter = 0

def increment_counter():
    global counter  # Declare we're using the global variable
    counter += 1

print(f"Before: {counter}")  # Output: Before: 0
increment_counter()
increment_counter()
print(f"After: {counter}")   # Output: After: 2

Nonlocal Variables (for nested functions)

def outer_function():
    outer_var = "I'm in outer function"
    
    def inner_function():
        nonlocal outer_var  # Refers to variable in the nearest enclosing scope
        outer_var = "Modified by inner function"
        print(inner_function)
    
    inner_function()
    print(outer_var)  # Output: Modified by inner function

outer_function()

Function Types

Pure Functions

# Pure function - same input always gives same output, no side effects
def multiply_pure(x, y):
    """Pure function example"""
    return x * y

result = multiply_pure(5, 3)
print(result)  # Output: 15

Higher-Order Functions

# Functions that take other functions as parameters or return functions
def apply_operation(numbers, operation):
    """Apply operation to each number in the list"""
    return [operation(num) for num in numbers]

def square(x):
    return x * x

def double(x):
    return x * 2

numbers = [1, 2, 3, 4, 5]
squared = apply_operation(numbers, square)
doubled = apply_operation(numbers, double)

print(squared)  # Output: [1, 4, 9, 16, 25]
print(doubled)  # Output: [2, 4, 6, 8, 10]

Closure Functions

def make_multiplier(factor):
    """Return a function that multiplies by the given factor"""
    def multiplier(x):
        return x * factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))  # Output: 10
print(triple(5))  # Output: 15

Generator Functions

def countdown(n):
    """Generator function that counts down from n to 0"""
    while n >= 0:
        yield n
        n -= 1

# Using the generator
for number in countdown(5):
    print(number)  # Output: 5, 4, 3, 2, 1, 0

Lambda Functions

Basic Lambda Syntax

# lambda arguments: expression
square = lambda x: x * x
print(square(5))  # Output: 25

Using Lambda with Built-in Functions

# Using lambda with map()
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared)  # Output: [1, 4, 9, 16, 25]

# Using lambda with filter()
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # Output: [2, 4]

# Using lambda with sorted()
points = [(1, 2), (3, 1), (5, 0), (2, 4)]
sorted_points = sorted(points, key=lambda point: point[1])
print(sorted_points)  # Output: [(5, 0), (3, 1), (1, 2), (2, 4)]

Practical Lambda Examples

# Simple calculator operations
operations = {
    'add': lambda x, y: x + y,
    'subtract': lambda x, y: x - y,
    'multiply': lambda x, y: x * y,
    'divide': lambda x, y: x / y if y != 0 else 'Undefined'
}

result = operations['add'](10, 5)
print(result)  # Output: 15

result = operations['multiply'](10, 5)
print(result)  # Output: 50

Decorators

Basic Decorator

def simple_decorator(func):
    """A simple decorator that adds functionality to a function"""
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@simple_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

Decorator with Arguments

def repeat(num_times):
    """Decorator that repeats function call specified number of times"""
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

greet("Alice")
# Output:
# Hello Alice
# Hello Alice
# Hello Alice

Practical Decorator Example: Timing Functions

import time

def timer(func):
    """Decorator that measures function execution time"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    """Function that takes some time to execute"""
    time.sleep(2)
    return "Done"

result = slow_function()  # Output: slow_function executed in 2.0023 seconds

Recursion

Basic Recursion Example

def factorial(n):
    """Calculate factorial using recursion"""
    if n == 1:  # Base case
        return 1
    else:       # Recursive case
        return n * factorial(n - 1)

print(factorial(5))  # Output: 120

Fibonacci Sequence with Recursion

def fibonacci(n):
    """Return nth Fibonacci number using recursion"""
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

# Print first 10 Fibonacci numbers
for i in range(10):
    print(fibonacci(i), end=" ")  # Output: 0 1 1 2 3 5 8 13 21 34

Practical Recursion: Directory Traversal

import os

def list_files(startpath):
    """Recursively list all files in a directory"""
    for root, dirs, files in os.walk(startpath):
        level = root.replace(startpath, '').count(os.sep)
        indent = ' ' * 2 * level
        print(f"{indent}{os.path.basename(root)}/")
        subindent = ' ' * 2 * (level + 1)
        for file in files:
            print(f"{subindent}{file}")

# Example usage (commented out to avoid actual directory listing):
# list_files('/path/to/directory')

Best Practices

Writing Good Functions

  1. Use descriptive names: Function names should clearly indicate what they do
  2. Keep functions small and focused: Each function should do one thing well
  3. Use docstrings: Document what your function does, its parameters, and return values
  4. Limit parameters: Functions with too many parameters are hard to use and understand
  5. Avoid side effects: Functions should generally not modify external state

Example of Well-Designed Function

def calculate_circle_area(radius):
    """
    Calculate the area of a circle.
    
    Args:
        radius (float): The radius of the circle
        
    Returns:
        float: The area of the circle
        
    Raises:
        ValueError: If radius is negative
    """
    if radius < 0:
        raise ValueError("Radius cannot be negative")
    
    return 3.14159 * radius ** 2

try:
    area = calculate_circle_area(5)
    print(f"Area: {area:.2f}")  # Output: Area: 78.54
except ValueError as e:
    print(e)

Type Hints (Python 3.5+)

def process_data(data: list[str], reverse: bool = False) -> dict[str, int]:
    """
    Process a list of strings and return a dictionary with word counts.
    
    Args:
        data: List of strings to process
        reverse: Whether to reverse the processing order
        
    Returns:
        Dictionary with words as keys and counts as values
    """
    result = {}
    processed_data = reversed(data) if reverse else data
    
    for item in processed_data:
        words = item.split()
        for word in words:
            result[word] = result.get(word, 0) + 1
    
    return result

sample_data = ["hello world", "world of python", "hello python"]
result = process_data(sample_data)
print(result)  # Output: {'hello': 2, 'world': 2, 'of': 1, 'python': 2}

Common Use Cases

Data Processing Functions

def process_csv_data(file_path, delimiter=','):
    """
    Read and process CSV data into a list of dictionaries
    
    Args:
        file_path: Path to the CSV file
        delimiter: Character used to separate values
        
    Returns:
        List of dictionaries representing each row
    """
    data = []
    with open(file_path, 'r') as file:
        headers = file.readline().strip().split(delimiter)
        for line in file:
            values = line.strip().split(delimiter)
            row = dict(zip(headers, values))
            data.append(row)
    return data

# Example usage (would need a actual CSV file):
# data = process_csv_data('data.csv')
# print(data)

Validation Functions

def validate_email(email):
    """
    Validate an email address format
    
    Args:
        email: Email address to validate
        
    Returns:
        bool: True if valid, False otherwise
    """
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

emails = ["test@example.com", "invalid-email", "another.test@domain.co.uk"]
for email in emails:
    print(f"{email}: {validate_email(email)}")
# Output:
# test@example.com: True
# invalid-email: False
# another.test@domain.co.uk: True

Utility Functions

def format_currency(amount, currency="USD"):
    """
    Format a number as currency
    
    Args:
        amount: Numerical amount to format
        currency: Currency code (USD, EUR, GBP, etc.)
        
    Returns:
        Formatted currency string
    """
    currency_symbols = {
        "USD": "$",
        "EUR": "€",
        "GBP": "£",
        "JPY": "¥"
    }
    
    symbol = currency_symbols.get(currency, "")
    return f"{symbol}{amount:,.2f}"

print(format_currency(1234.56))          # Output: $1,234.56
print(format_currency(9876.54, "EUR"))   # Output: €9,876.54
print(format_currency(5432.10, "JPY"))   # Output: ¥5,432.10

This comprehensive guide covers the essential aspects of user-defined functions in Python, from basic syntax to advanced concepts like decorators and recursion. Mastering functions is crucial for writing clean, efficient, and maintainable Python code.